'use client'; import { use, useEffect, useState, useRef } from 'react'; import * as signalR from '@microsoft/signalr'; import { DonationAlertData, DonationRemoteState } from '@/types/donation'; import { fetchApi } from '@/lib/utils/client'; import './style.scss'; type Props = { params: Promise<{ widgetToken: string }>; }; type AlertQueueItem = { alertID: number; donationID: number; status: string; sponsorMemberID: number; sendName: string; amount: number; message: string|null; channelName?: string; createdAt: string; }; const STATUS_LABEL: Record = { playing: '재생 중', queued: '대기', failed: '실패', delivered: '완료', skipped: '건너뜀', ignored: '무시' }; export default function RemotePage({ params }: Props) { const { widgetToken } = use(params); const apiBase = '/api/donation/remote'; const hubUrl = process.env.NEXT_PUBLIC_API_URL + '/hubs/donation'; const [connected, setConnected] = useState(false); const [state, setState] = useState({ isPaused: false, isAccepting: true, isAudioOnly: false, isVideoOnly: false }); const [queue, setQueue] = useState([]); const [openMenuID, setOpenMenuID] = useState(null); const connectionRef = useRef(null); // 초기 데이터 로드 + SignalR 연결 useEffect(() => { loadState(); connectHub(); return () => { connectionRef.current?.stop(); }; }, []); const loadState = async () => { try { const res = await fetchApi(`${apiBase}/state/${widgetToken}`, { silent: true }); if (res.data) { setState({ isPaused: res.data.isPaused, isAccepting: res.data.isAccepting, isAudioOnly: res.data.isAudioOnly, isVideoOnly: res.data.isVideoOnly }); setQueue(res.data.queue || []); } } catch {} }; const connectHub = () => { const conn = new signalR.HubConnectionBuilder().withUrl(hubUrl).withAutomaticReconnect().build(); conn.on('ReceiveAlert', (data: DonationAlertData) => { setQueue(prev => [...prev, { alertID: data.alertID, donationID: data.donationID, status: 'queued', sponsorMemberID: data.sponsorMemberID, sendName: data.sendName, amount: data.amount, message: data.message, createdAt: data.createdAt }]); }); conn.on('ReceiveState', (s: DonationRemoteState) => setState(s)); conn.on('ReceiveSkip', () => { setQueue(prev => prev.map(q => q.status === 'playing' ? { ...q, status: 'skipped' } : q)); }); conn.start().then(() => { conn.invoke('JoinChannel', widgetToken); setConnected(true); }).catch(() => {}); conn.onclose(() => setConnected(false)); conn.onreconnected(() => { conn.invoke('JoinChannel', widgetToken); setConnected(true); }); connectionRef.current = conn; }; // 리모콘 액션 const togglePause = async () => { const next = { ...state, isPaused: !state.isPaused }; setState(next); await fetchApi(`${apiBase}/state`, { method: 'POST', body: { ...next, widgetToken }, silent: true }); }; const toggleAccepting = async () => { const next = { ...state, isAccepting: !state.isAccepting }; setState(next); await fetchApi(`${apiBase}/state`, { method: 'POST', body: { ...next, widgetToken }, silent: true }); }; const toggleAudioOnly = async () => { const next = { ...state, isAudioOnly: !state.isAudioOnly, isVideoOnly: false }; setState(next); await fetchApi(`${apiBase}/state`, { method: 'POST', body: { ...next, widgetToken }, silent: true }); }; const toggleVideoOnly = async () => { const next = { ...state, isVideoOnly: !state.isVideoOnly, isAudioOnly: false }; setState(next); await fetchApi(`${apiBase}/state`, { method: 'POST', body: { ...next, widgetToken }, silent: true }); }; const skipCurrent = async () => { await fetchApi(`${apiBase}/skip/${widgetToken}`, { method: 'POST', silent: true }); }; const ignoreAlert = async (alertID: number) => { await fetchApi(`${apiBase}/ignore/${alertID}`, { method: 'POST', silent: true }); setQueue(prev => prev.map(q => q.alertID === alertID ? { ...q, status: 'ignored' } : q)); setOpenMenuID(null); }; const resendAlert = async (alertID: number) => { await fetchApi(`${apiBase}/resend/${alertID}`, { method: 'POST', silent: true }); setQueue(prev => prev.map(q => q.alertID === alertID ? { ...q, status: 'queued' } : q)); setOpenMenuID(null); }; const formatTime = (dateStr: string) => { const d = new Date(dateStr); return `${d.getHours().toString().padStart(2, '0')}:${d.getMinutes().toString().padStart(2, '0')}`; }; return (

리모콘

{connected ? '연결됨' : '연결 끊김'}
{/* 컨트롤 패널 */}
{/* 후원 목록 */}

후원 목록

{queue.length}건
{queue.length === 0 &&
후원 알림이 없습니다
} {queue.map(item => (
{/* 방향 아이콘 */}
{item.sendName}
{/* 본문 */}
{item.sendName} {item.amount.toLocaleString()}원
{item.message &&
{item.message}
}
{formatTime(item.createdAt)}
{/* 상태 뱃지 */} {STATUS_LABEL[item.status] || item.status} {/* 햄버거 메뉴 (재생 중이 아닐 때만) */} {item.status !== 'playing' && (
{openMenuID === item.alertID && (
{(item.status === 'failed' || item.status === 'skipped') && (
resendAlert(item.alertID)}>재전송
)} {item.status === 'queued' && (
ignoreAlert(item.alertID)}>무시
)}
)}
)}
))}
); }